package com.laamella.syntherface.synths.midi;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Transmitter;

import com.laamella.syntherface.synths.events.SynthWideEvents;

public class MidiInterface {

	private static final int NonRpnParameterLsb = 0x62;
	private static final int NonRpnParameterMsb = 0x63;
	private static final int RpnParameterLsb = 0x64;
	private static final int RpnParameterMsb = 0x65;
	private static final int DataEntryMsb = 0x06;
	private static final int DataEntryLsb = 0x26;

	private MidiDevice receiverDevice = null;
	private MidiDevice transmitterDevice = null;
	private Receiver receiver = null;
	private Transmitter transmitter = null;
	public boolean updateSynthesizer = true;
	public int sysExTimeout = 2000;

	public Receiver getReceiver() {
		return receiver;
	}

	public Transmitter getTransmitter() {
		return transmitter;
	}

	public void openReceiver(MidiDevice.Info receiverMidiDeviceInfo) throws MidiUnavailableException {
		closeReceiver();
		System.out.println("Opening receiver " + receiverMidiDeviceInfo.getName());
		receiverDevice = MidiSystem.getMidiDevice(receiverMidiDeviceInfo);
		receiverDevice.open();
		receiver = receiverDevice.getReceiver();
	}

	public void openTransmitter(MidiDevice.Info transmitterMidiDeviceInfo) throws MidiUnavailableException {
		closeTransmitter();
		System.out.println("Opening transmitter " + transmitterMidiDeviceInfo.getName());
		transmitterDevice = MidiSystem.getMidiDevice(transmitterMidiDeviceInfo);
		transmitterDevice.open();
		transmitter = transmitterDevice.getTransmitter();
	}

	public void closeReceiver() {
		if (receiver != null) {
			receiver.close();
			receiver = null;
		}

		if (receiverDevice != null && receiverDevice.isOpen()) {
			receiverDevice.close();
			receiverDevice = null;
		}
	}

	public void closeTransmitter() {
		if (transmitter != null) {
			transmitter.close();
			transmitter = null;
		}

		if (transmitterDevice != null && transmitterDevice.isOpen()) {
			transmitterDevice.close();
			transmitterDevice = null;
		}
	}

	public void closeAll() {
		closeReceiver();
		closeTransmitter();
	}

	public void sendNoteOnMessage(int note) throws InvalidMidiDataException {
		send(getShortMessage(ShortMessage.NOTE_ON, note));
	}

	public void sendNoteOffMessage(int note) throws InvalidMidiDataException {
		send(getShortMessage(ShortMessage.NOTE_OFF, note));
	}

	public void sendControlChange(int value) throws InvalidMidiDataException {
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, DataEntryMsb, Bits.msb(value)));
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, DataEntryLsb, Bits.lsb(value)));
	}

	public void sendNrpnMessage(int parameter, int value) throws InvalidMidiDataException {
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, NonRpnParameterLsb, Bits.lsb(parameter)));
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, NonRpnParameterMsb, Bits.msb(parameter)));
		sendControlChange(value);
	}

	public void sendRpnMessage(int parameter, int value) throws InvalidMidiDataException {
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, RpnParameterMsb, Bits.msb(parameter)));
		send(getShortMessage(ShortMessage.CONTROL_CHANGE, RpnParameterLsb, Bits.lsb(parameter)));
		sendControlChange(value);
	}

	private void send(MidiMessage message) {
		if (receiver != null && updateSynthesizer) {
			receiver.send(message, -1);
		}
	}

	private MidiMessage getShortMessage(int cmd, int data1) throws InvalidMidiDataException {
		ShortMessage message = new ShortMessage();
		message.setMessage(cmd, 0, data1, 60);
		return message;
	}

	private MidiMessage getShortMessage(int cmd, int data1, int data2) throws InvalidMidiDataException {
		ShortMessage message = new ShortMessage();
		message.setMessage(cmd, 0, data1, data2);
		return message;
	}

	/**
	 * For reverse engineering midi protocols :)
	 */
	public class Hacking {
		public void sendABunchOfNrpns(int startNrpn, int endNrpn, int valueToSetThemTo) {
			for (int i = startNrpn; i <= endNrpn; i++) {
				SynthWideEvents.statusUpdate().send("nrpn " + i);
				try {
					sendNrpnMessage(i, valueToSetThemTo);
				} catch (InvalidMidiDataException ex) {
					SynthWideEvents.statusUpdate().send(ex.getMessage());
				}
			}
		}
	}

	public void getResponseToSysEx(BinaryData requestMessage, BinaryData responseData) throws Exception {
		byte[] response = getResponseToSysEx(requestMessage.encode());
		if (response == null) {
			throw new Exception("No response from synthesizer!");
		}
		responseData.decode(response);
	}

	public void sendSysEx(byte[] message) throws InvalidMidiDataException {
		SysexMessage msg = new SysexMessage();
		msg.setMessage(message, message.length);
		getReceiver().send(msg, -1);
	}
	
	public void sendSysEx(BinaryData message) throws Exception{
		sendSysEx(message.encode());
	}

	public byte[] getResponseToSysEx(byte[] sysExMessage) {
		try {
			SysExReceiver sysExReceiver = new SysExReceiver();

			getTransmitter().setReceiver(sysExReceiver);

			sendSysEx(sysExMessage);
			System.out.println("Waiting for response...");
			int timeoutLeft = sysExTimeout;
			while (timeoutLeft > 0) {
				timeoutLeft -= 200;
				if (sysExReceiver.response != null) {
					System.out.println("Got a response!");
					SysexMessage response = sysExReceiver.response;
					byte[] result = new byte[response.getLength()];
					System.arraycopy(response.getMessage(), 0, result, 0, response.getLength());
					return result;
				}
				System.out.println("Waiting...");
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
				}
			}
		} catch (InvalidMidiDataException e) {
			SynthWideEvents.statusUpdate().send(e.getMessage());
		} finally {
			getTransmitter().setReceiver(null);
		}
		return null;
	}

	private void send(String message) {
		// TODO Auto-generated method stub
		
	}

	private static class SysExReceiver implements Receiver {
		public SysexMessage response = null;

		@Override public void close() {
		}

		@Override public void send(MidiMessage message, long timeStamp) {
			if (message instanceof SysexMessage) {
				response = (SysexMessage) message;
			}
		}

	}

}
